/*
Copyright 2023 The Radius Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

	http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package builder

import (
	"strings"
	"time"

	v1 "github.com/radius-project/radius/pkg/armrpc/api/v1"
	asyncctrl "github.com/radius-project/radius/pkg/armrpc/asyncoperation/controller"
	"github.com/radius-project/radius/pkg/armrpc/asyncoperation/worker"
	"github.com/radius-project/radius/pkg/armrpc/frontend/controller"
	"github.com/radius-project/radius/pkg/armrpc/frontend/defaultoperation"
	"github.com/radius-project/radius/pkg/armrpc/frontend/server"
)

const customActionPrefix = "ACTION"

// Operation defines converters for request and response, update and delete filters,
// asynchronous operation controller, and the options for API operation.
type Operation[T any] struct {
	// Disabled indicates that the operation is disabled. By default, all operations are enabled.
	Disabled bool

	// UpdateFilters is a slice of filters that execute prior to updating a resource.
	UpdateFilters []controller.UpdateFilter[T]

	// DeleteFilters is a slice of filters that execute prior to deleting a resource.
	DeleteFilters []controller.DeleteFilter[T]

	// APIController is the controller function for the API controller frontend.
	APIController server.ControllerFactoryFunc

	// AsyncJobController is the controller function for the async job worker.
	AsyncJobController worker.ControllerFactoryFunc

	// AsyncOperationTimeout is the default timeout duration of async operations for the operation.
	AsyncOperationTimeout time.Duration

	// AsyncOperationRetryAfter is the value of the Retry-After header that will be used for async operations.
	// If this is 0 then the default value of v1.DefaultRetryAfter will be used. Consider setting this to a smaller
	// value like 5 seconds if your operations will complete quickly.
	AsyncOperationRetryAfter time.Duration
}

// ResourceOption is the option for ResourceNode. It defines model converters for request and response
// and configures operation for each CRUDL and custom actions.
type ResourceOption[P interface {
	*T
	v1.ResourceDataModel
}, T any] struct {
	// linkedNode references to ResourceNode linked to this option.
	linkedNode *ResourceNode

	// ResourceParamName is the parameter name of the resource. This is optional.
	// If not set, the parameter name will be generated by adding "Name" suffix to the resource name.
	ResourceParamName string

	// RequestConverter is the request converter.
	RequestConverter v1.ConvertToDataModel[T]

	// ResponseConverter is the response converter.
	ResponseConverter v1.ConvertToAPIModel[T]

	// ListPlane defines the operation for listing resources by plane scope.
	ListPlane Operation[T]

	// List defines the operation for listing resources by resource group scope.
	List Operation[T]

	// Get defines the operation for getting a resource.
	Get Operation[T]

	// Put defines the operation for creating or updating a resource.
	Put Operation[T]

	// Patch defines the operation for updating a resource.
	Patch Operation[T]

	// Delete defines the operation for deleting a resource.
	Delete Operation[T]

	// Custom defines the custom actions.
	Custom map[string]Operation[T]
}

// LinkResource links the resource node to the resource option.
func (r *ResourceOption[P, T]) LinkResource(node *ResourceNode) {
	r.linkedNode = node
}

// ParamName returns the parameter name of the resource.
// If ResourceParamName is not set, the parameter name will be generated by adding "Name" suffix to the resource name.
func (r *ResourceOption[P, T]) ParamName() string {
	if r.ResourceParamName == "" {
		typeName := r.linkedNode.Name
		if strings.HasSuffix(typeName, "s") {
			return typeName[:len(typeName)-1] + "Name"
		} else {
			return typeName + "Name"
		}
	}

	return r.ResourceParamName
}

// BuildHandlerOutputs builds the handler outputs for each operation.
func (r *ResourceOption[P, T]) BuildHandlerOutputs(opts BuildOptions) []*OperationRegistration {
	handlerFuncs := []func(opts BuildOptions) *OperationRegistration{
		r.listPlaneOutput,
		r.listOutput,
		r.getOutput,
		r.putOutput,
		r.patchOutput,
		r.deleteOutput,
	}

	hs := []*OperationRegistration{}
	for _, h := range handlerFuncs {
		if out := h(opts); out != nil {
			hs = append(hs, out)
		}
	}

	return append(hs, r.customActionOutputs(opts)...)
}

func (r *ResourceOption[P, T]) listPlaneOutput(opts BuildOptions) *OperationRegistration {
	if r.ListPlane.Disabled || r.linkedNode.Kind != TrackedResourceKind {
		return nil
	}

	h := &OperationRegistration{
		ResourceType:        opts.ResourceType,
		ResourceNamePattern: opts.ResourceNamePattern,
		Method:              v1.OperationPlaneScopeList,
	}

	if r.ListPlane.APIController != nil {
		h.APIController = r.ListPlane.APIController
	} else {
		h.APIController = func(opt controller.Options) (controller.Controller, error) {
			return defaultoperation.NewListResources[P, T](opt,
				controller.ResourceOptions[T]{
					ResponseConverter:  r.ResponseConverter,
					ListRecursiveQuery: true,
				},
			)
		}
	}

	return h
}

func (r *ResourceOption[P, T]) listOutput(opts BuildOptions) *OperationRegistration {
	if r.List.Disabled {
		return nil
	}

	h := &OperationRegistration{
		ResourceType:        opts.ResourceType,
		ResourceNamePattern: opts.ResourceNamePattern,
		Method:              v1.OperationList,
	}

	if r.List.APIController != nil {
		h.APIController = r.List.APIController
	} else {
		h.APIController = func(opt controller.Options) (controller.Controller, error) {
			return defaultoperation.NewListResources[P, T](opt,
				controller.ResourceOptions[T]{
					ResponseConverter: r.ResponseConverter,
				},
			)
		}
	}

	return h
}

func (r *ResourceOption[P, T]) getOutput(opts BuildOptions) *OperationRegistration {
	if r.Get.Disabled {
		return nil
	}

	h := &OperationRegistration{
		ResourceType:        opts.ResourceType,
		ResourceNamePattern: opts.ResourceNamePattern + "/" + opts.ParameterName,
		Method:              v1.OperationGet,
	}

	if r.Get.APIController != nil {
		h.APIController = r.Get.APIController
	} else {
		h.APIController = func(opt controller.Options) (controller.Controller, error) {
			return defaultoperation.NewGetResource[P, T](opt,
				controller.ResourceOptions[T]{
					ResponseConverter: r.ResponseConverter,
				},
			)
		}
	}

	return h
}

func getOrDefaultAsyncOperationTimeout(d time.Duration) time.Duration {
	if d == 0 {
		return asyncctrl.DefaultAsyncOperationTimeout
	}
	return d
}

func getOrDefaultRetryAfter(d time.Duration) time.Duration {
	if d == 0 {
		return v1.DefaultRetryAfterDuration
	}
	return d
}

func (r *ResourceOption[P, T]) putOutput(opts BuildOptions) *OperationRegistration {
	if r.Put.Disabled {
		return nil
	}

	h := &OperationRegistration{
		ResourceType:        opts.ResourceType,
		ResourceNamePattern: opts.ResourceNamePattern + "/" + opts.ParameterName,
		Method:              v1.OperationPut,
		AsyncController:     r.Delete.AsyncJobController,
	}

	if r.Put.APIController != nil {
		h.APIController = r.Put.APIController
	} else {
		ro := controller.ResourceOptions[T]{
			RequestConverter:         r.RequestConverter,
			ResponseConverter:        r.ResponseConverter,
			UpdateFilters:            r.Put.UpdateFilters,
			AsyncOperationTimeout:    getOrDefaultAsyncOperationTimeout(r.Put.AsyncOperationTimeout),
			AsyncOperationRetryAfter: getOrDefaultRetryAfter(r.Put.AsyncOperationRetryAfter),
		}

		if r.Put.AsyncJobController == nil {
			h.APIController = func(opt controller.Options) (controller.Controller, error) {
				return defaultoperation.NewDefaultSyncPut[P, T](opt, ro)
			}
		} else {
			h.APIController = func(opt controller.Options) (controller.Controller, error) {
				return defaultoperation.NewDefaultAsyncPut[P, T](opt, ro)
			}
		}
	}
	h.AsyncController = r.Put.AsyncJobController

	return h
}

func (r *ResourceOption[P, T]) patchOutput(opts BuildOptions) *OperationRegistration {
	if r.Patch.Disabled {
		return nil
	}

	h := &OperationRegistration{
		ResourceType:        opts.ResourceType,
		ResourceNamePattern: opts.ResourceNamePattern + "/" + opts.ParameterName,
		Method:              v1.OperationPatch,
		AsyncController:     r.Patch.AsyncJobController,
	}

	if r.Patch.APIController != nil {
		h.APIController = r.Patch.APIController
	} else {
		ro := controller.ResourceOptions[T]{
			RequestConverter:         r.RequestConverter,
			ResponseConverter:        r.ResponseConverter,
			UpdateFilters:            r.Patch.UpdateFilters,
			AsyncOperationTimeout:    getOrDefaultAsyncOperationTimeout(r.Patch.AsyncOperationTimeout),
			AsyncOperationRetryAfter: getOrDefaultRetryAfter(r.Patch.AsyncOperationRetryAfter),
		}

		if r.Patch.AsyncJobController == nil {
			h.APIController = func(opt controller.Options) (controller.Controller, error) {
				return defaultoperation.NewDefaultSyncPut[P, T](opt, ro)
			}
		} else {
			h.APIController = func(opt controller.Options) (controller.Controller, error) {
				return defaultoperation.NewDefaultAsyncPut[P, T](opt, ro)
			}
		}
	}

	return h
}

func (r *ResourceOption[P, T]) deleteOutput(opts BuildOptions) *OperationRegistration {
	if r.Delete.Disabled {
		return nil
	}

	h := &OperationRegistration{
		ResourceType:        opts.ResourceType,
		ResourceNamePattern: opts.ResourceNamePattern + "/" + opts.ParameterName,
		Method:              v1.OperationDelete,
		AsyncController:     r.Delete.AsyncJobController,
	}

	if r.Delete.APIController != nil {
		h.APIController = r.Delete.APIController
	} else {
		ro := controller.ResourceOptions[T]{
			RequestConverter:         r.RequestConverter,
			ResponseConverter:        r.ResponseConverter,
			DeleteFilters:            r.Delete.DeleteFilters,
			AsyncOperationTimeout:    getOrDefaultAsyncOperationTimeout(r.Delete.AsyncOperationTimeout),
			AsyncOperationRetryAfter: getOrDefaultRetryAfter(r.Delete.AsyncOperationRetryAfter),
		}

		if r.Delete.AsyncJobController == nil {
			h.APIController = func(opt controller.Options) (controller.Controller, error) {
				return defaultoperation.NewDefaultSyncDelete[P, T](opt, ro)
			}
		} else {
			h.APIController = func(opt controller.Options) (controller.Controller, error) {
				return defaultoperation.NewDefaultAsyncDelete[P, T](opt, ro)
			}
		}
	}

	return h
}

func (r *ResourceOption[P, T]) customActionOutputs(opts BuildOptions) []*OperationRegistration {
	handlers := []*OperationRegistration{}

	for name, handle := range r.Custom {
		if handle.APIController == nil {
			panic("APIController is required for custom action")
		}

		h := &OperationRegistration{
			ResourceType:        opts.ResourceType,
			ResourceNamePattern: opts.ResourceNamePattern + "/" + opts.ParameterName,
			Path:                "/" + strings.ToLower(name),
			Method:              v1.OperationMethod(customActionPrefix + strings.ToUpper(name)),
			APIController:       handle.APIController,
			AsyncController:     handle.AsyncJobController,
		}
		handlers = append(handlers, h)
	}

	return handlers
}
